converts irc logs to html files that look like discord
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

273 lines
6.7 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. import { escapeHtmlShitty, HtmlEnt, JSX } from './jsx';
  2. const { readFileSync } = require("fs");
  3. const contents = readFileSync(0);
  4. const lines: string[] = contents.toString().split('\n');
  5. type MessageFrag = {
  6. type: 'url',
  7. link: string
  8. } | {
  9. type: 'text',
  10. content: string
  11. } | {
  12. type: 'error',
  13. content: string
  14. };
  15. type IrcUser = {
  16. mode?: '+' | '@' | ' ',
  17. name: string
  18. };
  19. type IrcMessage = {
  20. time: string,
  21. type: 'status',
  22. content: MessageFrag[]
  23. } | {
  24. time: string,
  25. type: 'action',
  26. user: IrcUser,
  27. content: MessageFrag[]
  28. } | {
  29. time: string,
  30. type: 'message',
  31. user: IrcUser,
  32. content: MessageFrag[]
  33. } | {
  34. type: 'day_change'
  35. } | {
  36. type: 'redacted'
  37. };
  38. const URL = /(((?:https?|gopher):\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/|spotify:track:)((?:\(?[^\s()<>]+\)?)*[^ \s`!\[\]{};:\'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))/i;
  39. // NOTE: When chaging, also update --pfp-size in style.css
  40. const pfp_size = 48;
  41. function parseMessage(m: string): MessageFrag[] {
  42. m = m.trimEnd();
  43. const parts: MessageFrag[] = [];
  44. let match: RegExpMatchArray | null;
  45. while ((match = m.match(URL)) != null) {
  46. parts.push({ type: 'text', content: m.slice(0, match.index) });
  47. parts.push({ type: 'url', link: match[0] });
  48. m = m.slice((match.index ?? 0) + match[0].length);
  49. }
  50. if (m !== "") {
  51. parts.push({ type: 'text', content: m });
  52. }
  53. return parts;
  54. }
  55. function parse(s: string): IrcMessage {
  56. s = s.trimEnd();
  57. if (s.startsWith("--- Day changed")) {
  58. return {
  59. type: "day_change"
  60. };
  61. } else if (s === "[REDACTED]") {
  62. return {
  63. type: "redacted"
  64. };
  65. }
  66. const time = s.slice(0, 5);
  67. switch (s[6]) {
  68. case '-':
  69. return {
  70. time,
  71. type: 'status',
  72. content: parseMessage(s.slice(10))
  73. };
  74. case '<': {
  75. const mode = s[7];
  76. if (mode !== '@' && mode !== '+' && mode !== ' ') throw `invalid mode: ${mode}`;
  77. const name = s.slice(8).split('>')[0];
  78. return {
  79. time,
  80. type: 'message',
  81. user: { mode, name },
  82. content: parseMessage(s.slice(10 + name.length))
  83. }
  84. }
  85. case ' ': {
  86. const name = s.slice(8).split(' ')[1];
  87. return {
  88. time,
  89. type: 'action',
  90. user: { name },
  91. content: parseMessage(s.slice(10 + name.length))
  92. }
  93. }
  94. }
  95. return {
  96. time: "error",
  97. type: 'status',
  98. content: [{
  99. type: 'error',
  100. content: `could not parse message: ${s}`
  101. }]
  102. };
  103. };
  104. const messages = lines.map(l => l.trim()).filter(l => l !== '.' && l !== "").map((v, i) => { try { return parse(v) } catch (e) { throw `${e} ${i}` } });
  105. type MsgGroup = {
  106. user?: IrcUser,
  107. type: "status" | "message" | "action" | "day_change" | "redacted",
  108. messages: IrcMessage[]
  109. }
  110. function group(messages: IrcMessage[]): MsgGroup[] {
  111. const groups: MsgGroup[] = [];
  112. let group: IrcMessage[] = [];
  113. let discrim: IrcUser | undefined = undefined;
  114. let type: "status" | "action" | "message" | undefined = undefined;
  115. for (const m of messages) {
  116. if (m.type === "status" || m.type === "day_change" || m.type == "redacted") {
  117. groups.push({
  118. user: discrim,
  119. type: type ?? "message",
  120. messages: group
  121. });
  122. discrim = undefined;
  123. type = "status";
  124. group = [];
  125. groups.push({
  126. type: m.type,
  127. messages: [m]
  128. })
  129. continue;
  130. } else if (discrim !== undefined && (m.user.mode === undefined || m.user.mode === discrim.mode) && m.user.name === discrim.name) {
  131. group.push(m)
  132. } else {
  133. if (group.length !== 0)
  134. groups.push({
  135. user: discrim,
  136. type: type ?? "message",
  137. messages: group
  138. });
  139. group = [m];
  140. discrim = m.user;
  141. type = m.type;
  142. }
  143. }
  144. if (group.length !== 0) {
  145. if (type === "status") {
  146. groups.push({
  147. user: undefined,
  148. type,
  149. messages: group
  150. })
  151. } else if (discrim !== undefined) {
  152. groups.push({
  153. user: discrim,
  154. type: type ?? "message",
  155. messages: group
  156. });
  157. }
  158. }
  159. return groups;
  160. }
  161. const groups = group(messages);
  162. const render = (c: MessageFrag): JSX.Element => {
  163. if (c.type === "text") {
  164. return JSX.createTextNode(escapeHtmlShitty(c.content));
  165. } else if (c.type === "url") {
  166. return <a href={c.link}>{escapeHtmlShitty(c.link)}</a>;
  167. } else if (c.type === "error") {
  168. return <span style="color: red; font-family: monospace">{escapeHtmlShitty(c.content)}</span>;
  169. } else {
  170. throw "impossible!" // thanks TypeScript
  171. }
  172. }
  173. const Message = ({ message, inclTs }: { message: IrcMessage, inclTs?: boolean }) => {
  174. if (message.type === "day_change") {
  175. return <div class="mg-sep">
  176. <span>Today</span>
  177. </div>
  178. };
  179. if (message.type === "redacted") {
  180. return <div class="mg-redacted">
  181. [REDACTED]
  182. </div>;
  183. }
  184. return <div class={`mg-m mg-${message.type}`}>
  185. {
  186. (inclTs === undefined || inclTs === true) ? <span class="mg-ts"> {message.time} </span> : undefined
  187. }
  188. <span class={`mg-txt mg-${message.type}`}>
  189. { message.content.map(c => render(c)) }
  190. </span>
  191. </div>
  192. }
  193. function renderMessageGroup(m: MsgGroup): HtmlEnt {
  194. const u = m.user;
  195. const m0 = m.messages[0];
  196. if (u !== undefined) {
  197. return <div class="mg-user">
  198. <div class="mg-pfp">
  199. <img src={`https://offtopia.org/pages/people/${u.name.toLocaleLowerCase()}.jpg`} alt={`${u.name}'s profile picture`} width={pfp_size} height={pfp_size} />
  200. </div>
  201. <div class="mg-contents">
  202. <span class="mg-ts-u">
  203. <span class="mg-ts-u-u"> {m.user?.name} </span>
  204. <span class="mg-ts-u-ts"> {(m0.type === "day_change" || m0.type === "redacted") ? "what" : m0.time} </span>
  205. </span>
  206. <Message message={m.messages[0]} inclTs={false} />
  207. {
  208. m.messages.slice(1).map(x =>
  209. <Message message={x} />
  210. )
  211. }
  212. </div>
  213. </div>
  214. } else {
  215. if (m.messages.length < 1) {
  216. return <hr style="display: none;" />
  217. }
  218. return <div class="mg-no-user">
  219. {m.messages.map(x => <Message message={x} />)}
  220. </div>;
  221. }
  222. }
  223. const rendered = groups.map(renderMessageGroup);
  224. const elem = <html lang="en">
  225. <head>
  226. <meta charset="UTF-8" />
  227. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  228. <meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
  229. </head>
  230. <body>
  231. {rendered}
  232. <svg id="squircle-container" width="0" height="0">
  233. <clipPath id="squircle" clipPathUnits="objectBoundingBox">
  234. <path fill="red" stroke="none" d="M 0,0.5 C 0,0 0,0 0.5,0 S 1,0 1,0.5 1,1 0.5,1 0,1 0,0.5" />
  235. </clipPath>
  236. </svg>
  237. </body>
  238. <style>
  239. {readFileSync("style.css")}
  240. </style>
  241. </html>;
  242. console.log(elem.toHtmlStr());