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.

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