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.

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